/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
-const Clutter = imports.gi.Clutter;
-const Lang = imports.lang;
-const Shell = imports.gi.Shell;
-const Signals = imports.signals;
-const St = imports.gi.St;
-const Mainloop = imports.mainloop;
const Gettext = imports.gettext.domain('gnome-shell');
const _ = Gettext.gettext;
const DocInfo = imports.misc.docInfo;
-const DND = imports.ui.dnd;
-const GenericDisplay = imports.ui.genericDisplay;
-const Main = imports.ui.main;
const Search = imports.ui.search;
-const MAX_DASH_DOCS = 50;
-const DASH_DOCS_ICON_SIZE = 16;
-
-const DEFAULT_SPACING = 4;
-
-/* This class represents a single display item containing information about a document.
- * We take the current number of seconds in the constructor to avoid looking up the current
- * time for every item when they are created in a batch.
- *
- * docInfo - DocInfo object containing information about the document
- * currentSeconds - current number of seconds since the epoch
- */
-function DocDisplayItem(docInfo, currentSecs) {
- this._init(docInfo, currentSecs);
-}
-
-DocDisplayItem.prototype = {
- __proto__: GenericDisplay.GenericDisplayItem.prototype,
-
- _init : function(docInfo, currentSecs) {
- GenericDisplay.GenericDisplayItem.prototype._init.call(this);
- this._docInfo = docInfo;
-
- this._setItemInfo(docInfo.name, '');
-
- this._timeoutTime = -1;
- this._resetTimeDisplay(currentSecs);
- },
-
- //// Public methods ////
-
- getUpdateTimeoutTime: function() {
- return this._timeoutTime;
- },
-
- // Update any relative-time based displays for this item.
- redisplay: function(currentSecs) {
- this._resetTimeDisplay(currentSecs);
- },
-
- //// Public method overrides ////
-
- // Opens a document represented by this display item.
- launch : function() {
- this._docInfo.launch();
- },
-
- //// Protected method overrides ////
-
- // Returns an icon for the item.
- _createIcon : function() {
- return this._docInfo.createIcon(GenericDisplay.ITEM_DISPLAY_ICON_SIZE);
- },
-
- // Returns a preview icon for the item.
- _createPreviewIcon : function() {
- return this._docInfo.createIcon(GenericDisplay.PREVIEW_ICON_SIZE);
- },
-
- // Creates and returns a large preview icon, but only if this._docInfo is an image file
- // and we were able to generate a pixbuf from it successfully.
- _createLargePreviewIcon : function() {
- if (this._docInfo.mimeType == null || this._docInfo.mimeType.indexOf('image/') != 0)
- return null;
-
- try {
- return St.TextureCache.get_default().load_uri_sync(St.TextureCachePolicy.NONE,
- this._docInfo.uri, -1, -1);
- } catch (e) {
- // An exception will be raised when the image format isn't know
- /* FIXME: http://bugzilla.gnome.org/show_bug.cgi?id=591480: should
- * only ignore GDK_PIXBUF_ERROR_UNKNOWN_TYPE. */
- return null;
- }
- },
-
- //// Drag and Drop ////
-
- shellWorkspaceLaunch: function() {
- this.launch();
- },
-
- //// Private Methods ////
-
- // Updates the last visited time displayed in the description text for the item.
- _resetTimeDisplay: function(currentSecs) {
- let lastSecs = this._docInfo.timestamp;
- let timeDelta = currentSecs - lastSecs;
- let [text, nextUpdate] = global.format_time_relative_pretty(timeDelta);
- this._timeoutTime = currentSecs + nextUpdate;
- this._setDescriptionText(text);
- }
-};
-
-/* This class represents a display containing a collection of document items.
- * The documents are sorted by how recently they were last visited.
- */
-function DocDisplay() {
- this._init();
-}
-
-DocDisplay.prototype = {
- __proto__: GenericDisplay.GenericDisplay.prototype,
-
- _init : function() {
- GenericDisplay.GenericDisplay.prototype._init.call(this);
- // We keep a single timeout callback for updating last visited times
- // for all the items in the display. This avoids creating individual
- // callbacks for each item in the display. So proper time updates
- // for individual items and item details depend on the item being
- // associated with one of the displays.
- this._updateTimeoutTargetTime = -1;
- this._updateTimeoutId = 0;
-
- this._docManager = DocInfo.getDocManager();
- this._docsStale = true;
- this._docManager.connect('changed', Lang.bind(this, function(mgr, userData) {
- this._docsStale = true;
- // Changes in local recent files should not happen when we are in the Overview mode,
- // but redisplaying right away is cool when we use Zephyr.
- // Also, we might be displaying remote documents, like Google Docs, in the future
- // which might be edited by someone else.
- this._redisplay(GenericDisplay.RedisplayFlags.NONE);
- }));
-
- this.connect('destroy', Lang.bind(this, function (o) {
- if (this._updateTimeoutId > 0)
- Mainloop.source_remove(this._updateTimeoutId);
- }));
- },
-
- //// Protected method overrides ////
-
- // Gets the list of recent items from the recent items manager.
- _refreshCache : function() {
- if (!this._docsStale)
- return true;
- this._allItems = {};
- Lang.copyProperties(this._docManager.getInfosByUri(), this._allItems);
- this._docsStale = false;
- return false;
- },
-
- // Sets the list of the displayed items based on how recently they were last visited.
- _setDefaultList : function() {
- // It seems to be an implementation detail of the Mozilla JavaScript that object
- // properties are returned during the iteration in the same order in which they were
- // defined, but it is not a guarantee according to this
- // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Statements/for...in
- // While this._allItems associative array seems to always be ordered by last added,
- // as the results of this._recentManager.get_items() based on which it is constructed are,
- // we should do the sorting manually because we want the order to be based on last visited.
- //
- // This function is called each time the search string is set back to '' or we display
- // the Overview, so we are doing the sorting over the same items multiple times if the list
- // of recent items didn't change. We could store an additional array of doc ids and sort
- // them once when they are returned by this._recentManager.get_items() to avoid having to do
- // this sorting each time, but the sorting seems to be very fast anyway, so there is no need
- // to introduce an additional class variable.
- this._matchedItems = {};
- this._matchedItemKeys = [];
- let docIdsToRemove = [];
- for (docId in this._allItems) {
- this._matchedItems[docId] = 1;
- this._matchedItemKeys.push(docId);
- }
-
- for (docId in docIdsToRemove) {
- delete this._allItems[docId];
- }
-
- this._matchedItemKeys.sort(Lang.bind(this, this._compareItems));
- },
-
- // Compares items associated with the item ids based on how recently the items
- // were last visited.
- // Returns an integer value indicating the result of the comparison.
- _compareItems : function(itemIdA, itemIdB) {
- let docA = this._allItems[itemIdA];
- let docB = this._allItems[itemIdB];
-
- return docB.timestamp - docA.timestamp;
- },
-
- // Checks if the item info can be a match for the search string by checking
- // the name of the document. Item info is expected to be GtkRecentInfo.
- // Returns a boolean flag indicating if itemInfo is a match.
- _isInfoMatching : function(itemInfo, search) {
- if (!itemInfo.exists())
- return false;
-
- if (search == null || search == '')
- return true;
-
- let name = itemInfo.name.toLowerCase();
- if (name.indexOf(search) >= 0)
- return true;
- // TODO: we can also check doc URIs, so that
- // if you search for a directory name, we display recent files from it
- return false;
- },
-
- // Creates a DocDisplayItem based on itemInfo, which is expected to be a DocInfo object.
- _createDisplayItem: function(itemInfo) {
- let currentSecs = new Date().getTime() / 1000;
- let docDisplayItem = new DocDisplayItem(itemInfo, currentSecs);
- this._updateTimeoutCallback(docDisplayItem, currentSecs);
- return docDisplayItem;
- },
-
- //// Private Methods ////
-
- // A callback function that redisplays the items, updating their descriptions,
- // and sets up a new timeout callback.
- _docTimeout: function () {
- let currentSecs = new Date().getTime() / 1000;
- this._updateTimeoutId = 0;
- this._updateTimeoutTargetTime = -1;
- for (let docId in this._displayedItems) {
- let docDisplayItem = this._displayedItems[docId];
- docDisplayItem.redisplay(currentSecs);
- this._updateTimeoutCallback(docDisplayItem, currentSecs);
- }
- return false;
- },
-
- // Updates the timeout callback if the timeout time for the docDisplayItem
- // is earlier than the target time for the current timeout callback.
- _updateTimeoutCallback: function (docDisplayItem, currentSecs) {
- let timeoutTime = docDisplayItem.getUpdateTimeoutTime();
- if (this._updateTimeoutTargetTime < 0 || timeoutTime < this._updateTimeoutTargetTime) {
- if (this._updateTimeoutId > 0)
- Mainloop.source_remove(this._updateTimeoutId);
- this._updateTimeoutId = Mainloop.timeout_add_seconds(timeoutTime - currentSecs, Lang.bind(this, this._docTimeout));
- this._updateTimeoutTargetTime = timeoutTime;
- }
- }
-};
-
-Signals.addSignalMethods(DocDisplay.prototype);
-
-function DashDocDisplayItem(docInfo) {
- this._init(docInfo);
-}
-
-DashDocDisplayItem.prototype = {
- _init: function(docInfo) {
- this._info = docInfo;
- this._icon = docInfo.createIcon(DASH_DOCS_ICON_SIZE);
-
- this.actor = new St.Clickable({ style_class: 'recent-docs-item',
- reactive: true,
- x_align: St.Align.START });
-
- let box = new St.BoxLayout({ style_class: 'recent-docs-item-box' });
- this.actor.set_child(box);
-
- box.add(this._icon);
-
- let text = new St.Label({ text: docInfo.name });
- box.add(text);
-
- this.actor.connect('clicked', Lang.bind(this, function () {
- docInfo.launch();
- Main.overview.hide();
- }));
-
- this.actor._delegate = this;
- let draggable = DND.makeDraggable(this.actor);
- draggable.connect('drag-begin',
- Lang.bind(this, function() {
- Main.overview.beginItemDrag(this);
- }));
- draggable.connect('drag-end',
- Lang.bind(this, function() {
- Main.overview.endItemDrag(this);
- }));
- },
-
- getUri: function() {
- return this._info.uri;
- },
-
- getDragActorSource: function() {
- return this._icon;
- },
-
- getDragActor: function(stageX, stageY) {
- this.dragActor = this._info.createIcon(DASH_DOCS_ICON_SIZE);
- return this.dragActor;
- },
-
- //// Drag and drop functions ////
-
- shellWorkspaceLaunch: function () {
- this._info.launch();
- }
-};
-
-/**
- * Class used to display two column recent documents in the dash
- */
-function DashDocDisplay() {
- this._init();
-}
-
-DashDocDisplay.prototype = {
- _init: function() {
- this.actor = new Shell.GenericContainer();
- this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
- this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
- this.actor.connect('allocate', Lang.bind(this, this._allocate));
- this._workId = Main.initializeDeferredWork(this.actor, Lang.bind(this, this._redisplay));
-
- this._actorsByUri = {};
-
- this._docManager = DocInfo.getDocManager();
- this._docManager.connect('changed', Lang.bind(this, this._onDocsChanged));
- this._pendingDocsChange = true;
- this._checkDocExistence = false;
- },
-
- _getPreferredWidth: function(actor, forHeight, alloc) {
- let children = actor.get_children();
-
- // We use two columns maximum. Just take the min and natural size of the
- // first two items, even though strictly speaking it's not correct; we'd
- // need to calculate how many items we could fit for the height, then
- // take the biggest preferred width for each column.
- // In practice the dash gets a fixed width anyways.
-
- // If we have one child, add its minimum and natural size
- if (children.length > 0) {
- let [minSize, naturalSize] = children[0].get_preferred_width(forHeight);
- alloc.min_size += minSize;
- alloc.natural_size += naturalSize;
- }
- // If we have two, add its size, plus DEFAULT_SPACING
- if (children.length > 1) {
- let [minSize, naturalSize] = children[1].get_preferred_width(forHeight);
- alloc.min_size += DEFAULT_SPACING + minSize;
- alloc.natural_size += DEFAULT_SPACING + naturalSize;
- }
- },
-
- _getPreferredHeight: function(actor, forWidth, alloc) {
- let children = actor.get_children();
-
- // The width of an item is our allocated width, minus spacing, divided in half.
- this._itemWidth = Math.floor((forWidth - DEFAULT_SPACING) / 2);
-
- let maxNatural = 0;
- for (let i = 0; i < children.length; i++) {
- let child = children[i];
- let [minSize, naturalSize] = child.get_preferred_height(this._itemWidth);
- maxNatural = Math.max(maxNatural, naturalSize);
- }
-
- this._itemHeight = maxNatural;
-
- let firstColumnChildren = Math.ceil(children.length / 2);
- alloc.natural_size = (firstColumnChildren * maxNatural +
- (firstColumnChildren - 1) * DEFAULT_SPACING);
- },
-
- _allocate: function(actor, box, flags) {
- let width = box.x2 - box.x1;
- let height = box.y2 - box.y1;
-
- // Make sure this._itemWidth/Height have been computed, even
- // if the parent actor didn't check our size before allocating.
- // (Not clear if that is required or not as a Clutter
- // invariant; this is safe and cheap because of caching.)
- actor.get_preferred_height(width);
-
- let children = actor.get_children();
-
- let x = 0;
- let y = 0;
- let columnIndex = 0;
- let i = 0;
- // Loop over the children, going vertically down first. When we run
- // out of vertical space (our y variable is bigger than box.y2), switch
- // to the second column.
- while (i < children.length) {
- let child = children[i];
-
- if (y + this._itemHeight > box.y2) {
- // Is this the second column, or we're in
- // the first column and can't even fit one
- // item? In that case, break.
- if (columnIndex == 1 || i == 0) {
- break;
- }
- // Set x to the halfway point.
- columnIndex += 1;
- x = x + this._itemWidth + DEFAULT_SPACING;
- // And y is back to the top.
- y = 0;
- // Retry this same item, now that we're in the second column.
- // By looping back to the top here, we re-test the size
- // again for the second column.
- continue;
- }
-
- let childBox = new Clutter.ActorBox();
- childBox.x1 = x;
- childBox.y1 = y;
- childBox.x2 = childBox.x1 + this._itemWidth;
- childBox.y2 = y + this._itemHeight;
-
- y = childBox.y2 + DEFAULT_SPACING;
-
- child.allocate(childBox, flags);
- this.actor.set_skip_paint(child, false);
-
- i++;
- }
-
- if (this._checkDocExistence) {
- // Now we know how many docs we are displaying, queue a check to see if any of them
- // have been deleted. If they are deleted, then we'll get a 'changed' signal; since
- // we'll now be displaying items we weren't previously, we'll check again to see
- // if they were deleted, and so forth and so on.
- // TODO: We should change this to ask for as many as we can fit in the given space:
- // https://bugzilla.gnome.org/show_bug.cgi?id=603522#c23
- this._docManager.queueExistenceCheck(i);
- this._checkDocExistence = false;
- }
-
- for (; i < children.length; i++)
- this.actor.set_skip_paint(children[i], true);
- },
-
- _onDocsChanged: function() {
- this._checkDocExistence = true;
- Main.queueDeferredWork(this._workId);
- },
-
- _redisplay: function() {
- // Should be kept alive by the _actorsByUri
- this.actor.remove_all();
- let docs = this._docManager.getTimestampOrderedInfos();
- for (let i = 0; i < docs.length && i < MAX_DASH_DOCS; i++) {
- let doc = docs[i];
- let display = this._actorsByUri[doc.uri];
- if (display) {
- this.actor.add_actor(display.actor);
- } else {
- let display = new DashDocDisplayItem(doc);
- this.actor.add_actor(display.actor);
- this._actorsByUri[doc.uri] = display;
- }
- }
- // Any unparented actors must have been deleted
- for (let uri in this._actorsByUri) {
- let display = this._actorsByUri[uri];
- if (display.actor.get_parent() == null) {
- display.actor.destroy();
- delete this._actorsByUri[uri];
- }
- }
- this.emit('changed');
- }
-};
-
-Signals.addSignalMethods(DashDocDisplay.prototype);
function DocSearchProvider() {
this._init();